/*
 * Copyright (c) 2008, 2009, 2010, 2011 Denis Tulskiy
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with this work.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.tulskiy.musique.audio.formats.mp3;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;

import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.BitstreamException;
import javazoom.jl.decoder.Decoder;
import javazoom.jl.decoder.DecoderException;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.SampleBuffer;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;

import com.tulskiy.musique.audio.IcyInputStream;
import com.tulskiy.musique.model.TrackData;
import com.tulskiy.musique.util.AudioMath;

/**
 * @Author: Denis Tulskiy
 * @Date: 12.06.2009
 */
public class MP3Decoder implements com.tulskiy.musique.audio.Decoder {
	private static final int DECODE_AFTER_SEEK = 9;
	private LinkedHashMap<File, SeekTable> seekTableCache = new LinkedHashMap<File, SeekTable>(
			10, 0.7f, true) {
		@Override
		protected boolean removeEldestEntry(Map.Entry<File, SeekTable> eldest) {
			return size() > 10;
		}
	};

	private Bitstream bitstream;
	private javazoom.jl.decoder.Decoder decoder;
	private Header readFrame;
	private TrackData trackData;

	private long totalSamples;
	private long streamSize;
	private int samplesPerFrame;
	private int sampleOffset = 0;
	private int encDelay;
	private long currentSample;
	private boolean streaming = false;
	private int oldBitrate;

	private AudioTrack audioTrack;

	private Header skipFrame() throws BitstreamException {
		readFrame = bitstream.readFrame();
		if (readFrame == null) {
			return null;
		}
		bitstream.closeFrame();

		return readFrame;
	}

	private int samplesToMinutes(long samples) {
		return (int) (samples / trackData.getSampleRate() / 60f);
	}

	@SuppressWarnings({ "ResultOfMethodCallIgnored" })
	private boolean createBitstream(long targetSample) {
		if (bitstream != null)
			bitstream.close();
		bitstream = null;
		try {
			File file = trackData.getFile();
			FileInputStream fis = new FileInputStream(file);

			// so we compute target frame first
			targetSample += encDelay;
			int targetFrame = (int) ((double) targetSample / samplesPerFrame);
			sampleOffset = (int) (targetSample - targetFrame * samplesPerFrame)
					* trackData.getFrameSize();

			// then we get the seek table or create it if needed
			SeekTable seekTable = seekTableCache.get(file);
			if (seekTable == null && samplesToMinutes(totalSamples) > 10) {
				seekTable = new SeekTable();
				seekTableCache.put(file, seekTable);
			}

			int currentFrame = 0;
			// if we have a point, use it
			if (seekTable != null) {
				SeekTable.SeekPoint seekPoint = seekTable.get(targetFrame
						- DECODE_AFTER_SEEK);
				fis.skip(seekPoint.offset);
				currentFrame = seekPoint.frame;
			}

			// then we create the bitstream
			bitstream = new Bitstream(fis);
			decoder = new javazoom.jl.decoder.Decoder();

			readFrame = null;
			for (int i = currentFrame; i < targetFrame - DECODE_AFTER_SEEK; i++) {
				skipFrame();
				// store frame's position
				if (seekTable != null && i % 10000 == 0) {
					seekTable.add(i, streamSize - bitstream.getPosition());
				}
			}

			// decode some frames to warm up the decoder
			int framesToDecode = targetFrame < DECODE_AFTER_SEEK ? targetFrame
					: DECODE_AFTER_SEEK;
			for (int i = 0; i < framesToDecode; i++) {
				readFrame = bitstream.readFrame();
				if (readFrame != null)
					decoder.decodeFrame(readFrame, bitstream);
				bitstream.closeFrame();
			}

			return true;
		} catch (IOException ex) {
			ex.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}

		return false;
	}

	public boolean open(final TrackData trackData) {
		if (trackData == null)
			return false;
		this.trackData = trackData;
		try {
			URI location = trackData.getLocation();
			InputStream fis;
			if (trackData.isFile()) {
				streaming = false;
				fis = new FileInputStream(trackData.getFile());
				streamSize = trackData.getFile().length();
			} else {
				trackData.setCodec("MP3 Stream");
				streaming = true;
				fis = IcyInputStream.create(trackData);
				decoder = new Decoder();
			}
			bitstream = new Bitstream(fis);
			Header header = bitstream.readFrame();
			encDelay = header.getEncDelay();
			int encPadding = header.getEncPadding();
			int sampleRate = header.frequency();
			int channels = header.mode() == Header.SINGLE_CHANNEL ? 1 : 2;
			trackData.setSampleRate(sampleRate);
			trackData.setChannels(channels);
			oldBitrate = trackData.getBitrate();
			samplesPerFrame = (int) (header.ms_per_frame() * header.frequency() / 1000);

			if (!streaming) {
				totalSamples = samplesPerFrame
						* (header.max_number_of_frames(streamSize) + header
								.min_number_of_frames(streamSize)) / 2;
				if (encPadding < totalSamples) {
					totalSamples -= encPadding;
				}
				totalSamples -= encDelay;
				bitstream.close();
				fis.close();
				createBitstream(0);
			}

			currentSample = 0;

			//
			//
			int mFrequency = trackData.getSampleRate();
			int mChannel = AudioFormat.CHANNEL_CONFIGURATION_STEREO;
			int mSampBit = AudioFormat.ENCODING_PCM_16BIT;
			// 获得构建对象的最小缓冲区大小
			int minBufSize = AudioTrack.getMinBufferSize(mFrequency, mChannel,
					mSampBit);
			audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, mFrequency,
					mChannel, mSampBit, minBufSize * 2, AudioTrack.MODE_STREAM);
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}

		return true;
	}

	@Override
	public AudioTrack getAudioTrack() {
		return audioTrack;
	}

	public void seekSample(long targetSample) {
		currentSample = targetSample;
		createBitstream(targetSample);
	}

	public int decode(byte[] buf) {
		try {
			readFrame = bitstream.readFrame();

			if (readFrame == null) {
				return -1;
			}

			if (readFrame.bitrate_instant() > 0)
				trackData.setBitrate(readFrame.bitrate_instant() / 1000);

			if (!streaming && currentSample >= totalSamples)
				return -1;
			SampleBuffer output = (SampleBuffer) decoder.decodeFrame(readFrame,
					bitstream);
			bitstream.closeFrame();
			int dataLen = output.getBufferLength() * 2;
			int len = dataLen - sampleOffset;
			if (dataLen == 0) {
				return 0;
			}

			currentSample += AudioMath.bytesToSamples(len,
					trackData.getFrameSize());

			if (!streaming && currentSample > totalSamples) {
				len -= AudioMath.samplesToBytes(currentSample - totalSamples,
						trackData.getFrameSize());
			}
			toByteArray(output.getBuffer(), sampleOffset / 2, len / 2, buf);
			sampleOffset = 0;
			readFrame = null;
			return len;
		} catch (BitstreamException e) {
			e.printStackTrace();
		} catch (DecoderException e) {
			e.printStackTrace();
		}
		return -1;
	}

	public void close() {
		if (bitstream != null)
			bitstream.close();
		trackData.setBitrate(oldBitrate);
		readFrame = null;
	}

	private void toByteArray(short[] samples, int offs, int len, byte[] dest) {
		int idx = 0;
		short s;
		while (len-- > 0) {
			s = samples[offs++];
			dest[idx++] = (byte) s;
			dest[idx++] = (byte) (s >>> 8);
		}
	}
}
